package com.znxz.hzqiuxm.ziniublog.global.security;

import com.znxz.hzqiuxm.ziniublog.global.common.SysConst;
import com.znxz.hzqiuxm.ziniublog.global.config.PropertiesValue;
import com.znxz.hzqiuxm.ziniublog.global.exception.ZNUserException;
import com.znxz.hzqiuxm.ziniublog.user.domain.valobj.JwtUser;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * Copyright © 2018年 ziniuxiaozhu. All rights reserved.
 *
 * @Author 临江仙 hzqiuxm@163.com
 * JWT常用工具类
 * @Date 2018/8/31 17:06
 */

@Slf4j
@Component
public class JwtTokenUtil {


    @Autowired
    private PropertiesValue propertiesValue;

    //一分钟( 60 X 1000 )
    public static final int A_MINUTE = 600000;
    public static final String SHOW_NAME = "showname";
    public static final String ROLES = "roles";
    public static final String ISSUSER  = "qxkj";


    /**
     * 生成token
     * @param jwtUser
     * @return
     */
    public String generateToken(JwtUser jwtUser) {
        if (null == jwtUser){
            log.error("No successful login user");
            return "";
        }
        Map<String, Object> claims = new HashMap<>();
        claims.put(Claims.ID, jwtUser.getUserId());
        claims.put(Claims.SUBJECT, jwtUser.getUsername());
        claims.put(Claims.ISSUER, ISSUSER);
        claims.put(SHOW_NAME, jwtUser.getShowname());
        claims.put(ROLES, jwtUser.getAuthorities());
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(this.generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, propertiesValue.getSecret())
                .compact();
    }

    /**
     * 解析token,超过有效期也会解析失败
     * @param token
     * @return
     */
    public Claims getClaimsFromToken(String token) {
        Claims claims;
        try {

            claims = Jwts.parser()
                    .setSigningKey(propertiesValue.getSecret())
                    .require(Claims.ISSUER,"qxkj")
                    .parseClaimsJws(token)
                    .getBody();

        } catch (ExpiredJwtException e) {
            log.error("Token已过期: {} " + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_101, "Token已过期");
        } catch (UnsupportedJwtException e) {
            log.error("Token格式错误: {} " + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_102, "Token格式错误");
        } catch (MalformedJwtException e) {
            log.error("Token没有被正确构造: {} " + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_103, "Token没有被正确构造");
        } catch (SignatureException e) {
            log.error("签名失败: {} " + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_104, "签名失败");
        } catch (IllegalArgumentException e) {
            log.error("非法参数异常: {} " + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_105, "非法参数异常");
        } catch (MissingClaimException e) {
            log.error("缺少必要认证参数  :{}" + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_106, "缺少必要认证参数");
        } catch (IncorrectClaimException e) {
            log.error("token颁发认证机构不通过  :{}" + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_107, "token颁发认证机构不通过");
        }catch (Exception e){
            log.error("token未知原因验证失败  :{}" + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_108, "token未知原因验证失败");
        }
        return claims;
    }


    /**
     * 将token转换成jwtuser对象，但是没有密码和安全控制
     * @param token
     * @return
     */
    public JwtUser token2jwtuser(String token){

        Claims claims = getClaimsFromToken(token);
        String loginName = claims.getSubject();
        ObjectId userId = (ObjectId) claims.get("userId");
        String showname = (String) claims.get("showname");
        Date expiration = claims.getExpiration();

        ArrayList rolestemp = (ArrayList) claims.get(SysConst.ROLES);
        ArrayList<String>  roles = new ArrayList<>();
        for (int i = 0; i <rolestemp.size(); i++) {
            Map<String,String> role = (Map<String, String>) rolestemp.get(i);
            roles.add(role.get(SysConst.AUTHORITY));
        }

        return new JwtUser(
                userId,
                loginName,
                showname,
                null,
                null,
                null,
                roles,
                expiration);

    }

    /**
     * 验证token的合法性
     * 主要是验证颁发机构和有效期
     * @param compactJws
     * @return
     */
    public boolean verifyJWT(String compactJws) {
        Jws<Claims> claims = null;

        try {
            claims = Jwts.parser()
                    .setSigningKey(propertiesValue.getSecret())
                    .require(Claims.ISSUER,"qxkj")
                    .parseClaimsJws(compactJws);

            log.info("claims.getHeader() : {}", claims.getHeader().getAlgorithm());
            log.info("claims.getBody() : {}",claims.getBody().getSubject());
            log.info("claims.getSignature() : {}",claims.getSignature());


        } catch (ExpiredJwtException e) {
            log.error("Token已过期: {} " + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_101, "Token已过期");
        } catch (UnsupportedJwtException e) {
            log.error("Token格式错误: {} " + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_102, "Token格式错误");
        } catch (MalformedJwtException e) {
            log.error("Token没有被正确构造: {} " + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_103, "Token没有被正确构造");
        } catch (SignatureException e) {
            log.error("签名失败: {} " + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_104, "签名失败");
        } catch (IllegalArgumentException e) {
            log.error("非法参数异常: {} " + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_105, "非法参数异常");
        } catch (MissingClaimException e) {
            log.error("缺少必要认证参数  :{}" + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_106, "缺少必要认证参数");
        } catch (IncorrectClaimException e) {
            log.error("token颁发认证机构不通过  :{}" + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_107, "token颁发认证机构不通过");
        }catch (Exception e){
            log.error("token未知原因验证失败  :{}" + e.getMessage());
            throw new ZNUserException(SysConst.ReturnCode.F_108, "token未知原因验证失败");
        }
            return true;

    }

    private String createJWT(String id, String issuer, String subject, long ttlMillis) {

        //The JWT signature algorithm we will be using to sign the token
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        //We will sign our JWT with our ApiKey secret
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary("key");
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

        //Let's set the JWT Claims
        JwtBuilder builder = Jwts.builder().setId(id)
                .setIssuedAt(now)
                .setSubject(subject)
                .setIssuer(issuer)
                .signWith(signatureAlgorithm, signingKey);

        //if it has been specified, let's add the expiration
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }

        //Builds the JWT and serializes it to a compact, URL-safe string
        return builder.compact();
    }

    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + propertiesValue.getExpirationTime()*A_MINUTE);
    }

    /**
     * 是否要刷新token
     * @param expirationDate 过期时间
     * @return
     */
    public boolean canTokenBeRefreshed(Date expirationDate) {
        return (expirationDate.getTime() - System.currentTimeMillis()) < (propertiesValue.getRefresh()*A_MINUTE);
    }

}
